package com.ecommerce.service.impl;

import com.alibaba.fastjson.JSON;
import com.ecommerce.common.TableId;
import com.ecommerce.constant.GoodsConstant;
import com.ecommerce.dao.EcommerceGoodsDao;
import com.ecommerce.entity.EcommerceGoods;
import com.ecommerce.goods.GoodsInfo;
import com.ecommerce.goods.SimpleGoodsInfo;
import com.ecommerce.service.GoodsService;
import com.ecommerce.goods.DeductGoodsInventory;
import com.ecommerce.vo.PageSimpleGoodsInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 商品微服务相关功能实现
 */
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class GoodsServiceImpl implements GoodsService {

    private final StringRedisTemplate redisTemplate;

    private final EcommerceGoodsDao ecommerceGoodsDao;

    public GoodsServiceImpl(EcommerceGoodsDao ecommerceGoodsDao, StringRedisTemplate redisTemplate) {
        this.ecommerceGoodsDao = ecommerceGoodsDao;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public List<GoodsInfo> getGoodsInfoByTableId(TableId tableId) {
        // 详细的商品信息不能从RedisCache中拿到，所以此处必须查库
        List<Long> ids = tableId.getIds().stream()
                .map(TableId.Id::getId)
                .collect(Collectors.toList());
        log.info("get goods info by ids: [{}]", JSON.toJSONString(ids));

        List<EcommerceGoods> ecommerceGoods = IterableUtils.toList(
                ecommerceGoodsDao.findAllById(ids)
        );

        return ecommerceGoods.stream()
                .map(EcommerceGoods::toGoodsInfo)
                .collect(Collectors.toList());
    }

    @Override
    public PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page) {

        // 分页无法从RedisCache中去拿到
        if (page <= 1) {
            page = 1; // 默认是第一页
        }

        // 分页规则：一页十行, 按照商品的 id 倒序排列
        Pageable pageable = PageRequest.of(
                page - 1,
                10,
                Sort.by("id").descending()
        );

        Page<EcommerceGoods> orderPage = ecommerceGoodsDao.findAll(pageable);

        // 是否还有更多页：总页数是否大于当前给定的页
        boolean hasMore = orderPage.getTotalPages() > page;

        return new PageSimpleGoodsInfo(
                orderPage.getContent().stream()
                        .map(EcommerceGoods::toSimple)
                .collect(Collectors.toList()), hasMore
        );
    }

    /**
     * 将缓存中的数据反序列成 Java 对象
     */
    private List<SimpleGoodsInfo> parseCachedGoodsInfo(List<Object> cacheSimpleGoodsInfo) {
        return cacheSimpleGoodsInfo.stream()
                .map(s -> JSON.parseObject(s.toString(), SimpleGoodsInfo.class))
                .collect(Collectors.toList());
    }

    /**
     * 从数据表中查询数据并缓存到 redis 中, 这样就不是单一的依靠异步任务, 能够提高代码的健壮性
     */
    private List<SimpleGoodsInfo> queryGoodsFromDBAndCacheToRedis(TableId tableId) {
        // 从数据表中查询数据并作转换
        List<Long> ids = tableId.getIds().stream()
                .map(TableId.Id::getId)
                .collect(Collectors.toList());
        log.info("get simple goods info by ids (from db): [{}]",
                JSON.toJSONString(ids));

        List<EcommerceGoods> ecommerceGoods = IterableUtils.toList(
                ecommerceGoodsDao.findAllById(ids)
        );
        List<SimpleGoodsInfo> simpleGoodsInfos = ecommerceGoods.stream()
                .map(EcommerceGoods::toSimple)
                .collect(Collectors.toList());

        // 将结果缓存
        log.info("cache goods info: [{}]", JSON.toJSONString(ids));

        Map<String, String> idToJsonObject = new HashMap<>(simpleGoodsInfos.size());
        simpleGoodsInfos.forEach(
                g -> idToJsonObject.put(g.getId().toString(), JSON.toJSONString(g))
        );
        // 保存到 Redis 中
        redisTemplate.opsForHash().putAll(GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, idToJsonObject);

        return simpleGoodsInfos;
    }

    @Override
    public List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(TableId tableId) {

        // 获取商品的简单信息，可以从 redis cache 中去拿，拿不到需要从 DB 中获取并保存到 Redis 里面
        // Redis 中的 KV 都是字符串类型
        List<Object> goodIds = tableId.getIds().stream()
                .map(i -> i.getId().toString()).collect(Collectors.toList());

        // 如果不存在该数据会返回 null数组, 因此先要对空值做额外处理
        List<Object> cachedSimpleGoodsInfos = redisTemplate.opsForHash()
                .multiGet(GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, goodIds)
                .stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        // 如果从redis中查到了商品信息，分两种情况去操作
        if (CollectionUtils.isNotEmpty(cachedSimpleGoodsInfos)) {
            // 1. 如果从缓存种查询出所有需要的 SimpleGoodsInfo
            if (cachedSimpleGoodsInfos.size() == goodIds.size()) {
                log.info("get simple goods info by ids (from cache): [{}]", JSON.toJSONString(goodIds));
                return parseCachedGoodsInfo(cachedSimpleGoodsInfos);
            }else {
                // 2. 一半从数据库中获取，一般从缓存中获取
                List<SimpleGoodsInfo> left = parseCachedGoodsInfo(cachedSimpleGoodsInfos);
                // 取差集: 传递进来的参数 - 缓存中查到的 = 缓存中没有的
                Collection<Long> subtracts = CollectionUtils.subtract(
                        goodIds.stream()
                                .map(g -> Long.valueOf(g.toString()))
                        .collect(Collectors.toList()),
                        left.stream()
                        .map(SimpleGoodsInfo::getId).collect(Collectors.toList())
                );
                // 缓存中没有的去查询数据表并缓存
                List<SimpleGoodsInfo> right = queryGoodsFromDBAndCacheToRedis(
                        new TableId(subtracts.stream().map(TableId.Id::new).collect(Collectors.toList()))
                );
                // 合并 left 和 right 并返回
                log.info("get simple goods info by ids (from db and cache): [{}]",
                        JSON.toJSONString(subtracts));
                return new ArrayList<>(CollectionUtils.union(left, right));
            }
        }else {
            // cached中没有数据
            return queryGoodsFromDBAndCacheToRedis(tableId);
        }
    }

    @Override
    public Boolean deductGoodsInventory(List<DeductGoodsInventory> deductGoodsInventories) {

        // 检验参数是否合法
        deductGoodsInventories.forEach(d -> {
            if (d.getCount() <= 0) {
                throw new RuntimeException("purchase goods count need > 0");
            }
        });

        List<EcommerceGoods> ecommerceGoods = IterableUtils.toList(
                ecommerceGoodsDao.findAllById(
                        deductGoodsInventories.stream()
                        .map(DeductGoodsInventory::getGoodsId)
                        .collect(Collectors.toList())
                )
        );

        // 根据传递的 goodsIds 查询不到商品对象，抛异常
        if (CollectionUtils.isEmpty(ecommerceGoods)) {
            throw new RuntimeException("can not found any goods by request");
        }

        // 查询出来的商品与传递的不一致，抛异常
        if (ecommerceGoods.size() != deductGoodsInventories.size()) {
            throw new RuntimeException("request is not valid");
        }

        // goodsId -> DeductGoodsInventory
        Map<Long, DeductGoodsInventory> goodsIdToInventory = deductGoodsInventories.stream()
                .collect(Collectors.toMap(DeductGoodsInventory::getGoodsId, Function.identity()));

        // 检查是不是可以扣减库存，再去扣减库存
        ecommerceGoods.forEach(g -> {
                Long currentInventory = g.getInventory();
                Integer needDeductInventory = goodsIdToInventory.get(g.getId()).getCount();
                if (currentInventory < needDeductInventory) {
                    log.error("goods inventory is not enough: [{}], [{}]", currentInventory, needDeductInventory);
                    throw new RuntimeException("goods inventory is not enough: " + g.getId());
                }
                g.setInventory(currentInventory - needDeductInventory);
                log.info("deduct goods inventory: [{}], [{}], [{}]", g.getId(), currentInventory, g.getInventory());
            }
        );

        ecommerceGoodsDao.saveAll(ecommerceGoods);
        log.info("deduct goods inventory done");

        return true;
    }

}
